{# The one table macro. A page declares columns and hands over rows; it never writes a by hand. {% from "components/macros/table.html" import data_table %} {{ data_table(columns, rows, empty="No transactions yet") }} ``columns``: list of ``{"key", "label", "kind", "align", "signed"}``. ``kind`` is ``text`` (default), ``money`` (minor units, honours the row's ``currency`` when it has one; ``signed`` adds a leading ``+`` and colours by sign, ``toned`` colours by sign without the plus), ``date``, ``int``, ``status`` (the value is ``{"label", "tone"}``, rendered as a ``badge``), ``code`` (monospace, wrapping: a key, a rule, a command) or ``link`` (the value is ``{"label", "url"}``; the link replaces ``target``, default ``#app-content``, the way a list's row opens its item), ``avatar`` (the row's ``icon_url`` brand mark, or the initial, before the name); ``wrap`` lets a long text cell wrap instead of running on. A row marked ``group`` reads as a heading and the ``child`` rows after it are indented (a total and its parts under one set of columns); ``render`` is a macro taking the value, for a cell no kind covers; ``header`` is a macro rendering the column's heading in place of ``label`` (a select-all checkbox, say); ``align`` is ``left`` (default) or ``right``. Rows are objects or dicts; a blank value renders as a dash. Zero rows render ``empty_state`` instead. ``row_id`` names each ```` (``row_id ~ "-" ~ row.id``) so an action can swap one row back; a ``{% call(row) data_table(...) %}`` block, or a macro passed as ``actions``, adds an actions column rendered per row (``actions=none`` leaves it out, so one table serves both cases). ``detail`` is a macro taking the row: each row then gets a toggle and a hidden full-width row under it with that macro's output (the Flet expandable rows). #} {% from "components/macros/feedback.html" import empty_state %} {% from "components/macros/layout.html" import avatar, badge %} {% macro _cell(row, col) -%} {%- set value = row[col.key] if row is mapping or col.key is number else row|attr(col.key) -%} {%- if value is not defined or value is none or value == "" -%} - {%- elif col.render -%} {{ col.render(value) }} {%- elif col.kind == "money" -%} {%- set currency = (row["currency"] if row is mapping and "currency" in row else row|attr("currency")) -%} {{ "+" if col.signed and value > 0 }}{{ value|money(currency if currency is string else "USD") }} {%- elif col.kind == "status" -%} {{ badge(value.label, value.tone) }} {%- elif col.kind == "date" -%} {{ value|short_date }} {%- elif col.kind == "int" -%} {{ "{:,}".format(value) }} {%- elif col.kind == "avatar" -%} {%- set icon_url = row["icon_url"] if row is mapping and "icon_url" in row else row|attr("icon_url") -%} {{ avatar(value, icon_url if icon_url is string else None) }}{{ value }} {%- elif col.kind == "code" -%} {{ value }} {%- elif col.kind == "link" -%} {%- set link -%}{{ value.label }}{%- endset -%} {%- if col.mark -%} {%- set icon_url = row["icon_url"] if row is mapping and "icon_url" in row else row|attr("icon_url") -%} {{ avatar(value.label, icon_url if icon_url is string else None) }}{{ link }} {%- else -%}{{ link }}{%- endif -%} {%- else -%} {{ value }} {%- endif -%} {%- endmacro %} {# The text colour of one cell: signed money reads by sign. #} {% macro _tone(row, col) -%} {%- set value = row[col.key] if row is mapping or col.key is number else row|attr(col.key) -%} {%- if col.kind == "money" and (col.signed or col.toned) and value is number and value != 0 -%} {{ "text-aegis-teal" if value > 0 else "text-error" }} {%- else -%} text-aegis-text {%- endif -%} {%- endmacro %} {# One row. ``id`` lets an action swap this row back by itself; ``oob`` marks it as an out-of-band answer (pattern 2); ``actions`` is a macro (a call block's ``caller``) rendering the trailing actions cell. #} {% macro table_row(columns, row, id=None, oob=False, actions=None, expandable=False) %} {# A row swapped out of band travels wrapped: a bare at the top of a response bound for anything but a table is thrown away by the HTML parser, and its cells spill into whatever was being swapped — a stray row loose in a dialog. htmx unwraps a template before it swaps, so this is the way to send one anywhere. #} {% if oob %}{% endif %} {# A ``group`` row heads the ``child`` rows after it (a total and its parts, one set of columns); either flag rides on the row itself. #} {% set heading = row is mapping and row.get("group") %} {% set indented = row is mapping and row.get("child") %} {% if expandable %} {% endif %} {% for col in columns %} {% endfor %} {% if actions %}{% endif %} {% if oob %}{% endif %} {% endmacro %} {# ``flush`` is the card bleed: on by default, because a table spans its card; off where the caller already bleeds (a drawn card's table). #} {% macro data_table(columns, rows, empty="Nothing here yet", row_id=None, detail=None, actions=None, flush=True) %} {% set actions = actions or (caller if caller is defined else None) %} {% if rows %}
{% if detail %}{% endif %} {% for col in columns %} {% endfor %} {% if actions %}{% endif %} {% if detail %} {% for row in rows %} {{ table_row(columns, row, id=(row_id ~ "-" ~ (row["id"] if row is mapping else row.id)) if row_id else None, actions=actions, expandable=True) }} {% endfor %} {% else %} {% for row in rows %} {{ table_row(columns, row, id=(row_id ~ "-" ~ (row["id"] if row is mapping else row.id)) if row_id else None, actions=actions) }} {% endfor %} {% endif %}
Details{{ col.header() if col.header else col.label }}Actions
{{ detail(row) }}
{% else %} {{ empty_state(empty) }} {% endif %} {% endmacro %} {# Page navigation for a listed table. ``pager`` is the route's ``{start, end, total, prev, next}`` (``None`` when one page); the links re-request the same URL and swap ``target`` in place. #} {% macro pager(pager, target) %} {% if pager %} {% endif %} {% endmacro %}